package cn.coder.toolkit;

import org.apache.commons.compress.archivers.zip.ParallelScatterZipCreator;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.rmi.RemoteException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;

/**
 * 使用 Apache Commons Compress 工具包, 并行操作
 * 归档: 就是不压缩, 将多个文件打包到一起
 */
public class ZipKit {

    public static void main(String[] args) {

        try {


            long begin = System.currentTimeMillis();

            Path source = Paths.get("/Users/coder/Desktop/test");

            archive(source);

            System.out.println(System.currentTimeMillis() - begin);

        } catch (Throwable cause) {
            cause.printStackTrace();
        }

    }

    private static final String ZIP = ".zip";

    /**
     * 归档(不压缩)整个文件夹中的文件(递归)
     * @see ZipKit#archive(Path, Path)
     */
    public static void archive(Path source) throws IOException, ExecutionException, InterruptedException {
        if (Files.notExists(source)) {
            throw new RemoteException("Path not exist");
        }
        Path target = Paths.get(source.toString().concat(ZIP));
        archive(target, source);
    }

    /**
     * 归档(不压缩)整个文件夹中的文件(递归)
     * @param target 输出的归档文件
     * @param source 要归档的那个目录
     */
    public static void archive(Path target, Path source) throws IOException, ExecutionException, InterruptedException {
        if (Files.notExists(source)) {
            throw new RemoteException("Path not exist");
        }
        Path parent = source.getParent();
        List<Path> paths = listFilesByRecursive(source);
        Map<Path, List<Path>> map = paths.stream().collect(Collectors.groupingBy(item -> parent.relativize(item).getParent()));
        archive(target, map);
    }

    /**
     * 自定义归档(不压缩)
     * @param target 输出的归档文件
     * @param map 按不同的归档后目录层级分组后汇集起来的文件, 各文件将忽略自身目录层级, 使用 Key 指定的目录层级
     */
    public static void archive(Path target, Map<Path, List<Path>> map) throws IOException, ExecutionException, InterruptedException {
        if (Files.exists(target)) {
            Files.deleteIfExists(target);
        }
        if (Files.notExists(target.getParent())) {
            Files.createDirectories(target.getParent());
        }

        // ParallelScatterZipCreator 在用完后将会自动关闭
        ExecutorService executor = Executors.newFixedThreadPool(10);
        ParallelScatterZipCreator parallelScatterZipCreator = new ParallelScatterZipCreator(executor);

        // 写出的位置
        try (
                OutputStream outputStream = Files.newOutputStream(target);
                ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputStream);
            ) {
            // 文件名和注释的编码
            zipArchiveOutputStream.setEncoding("UTF-8");
            // zipArchiveOutputStream.setLevel(Deflater.NO_COMPRESSION); // 不压缩, 两个不压缩不知道有什么关联?

            // 将要归档的每个文件都转换成为 ZipArchiveEntry
            for (Map.Entry<Path, List<Path>> entry : map.entrySet()) {
                Path prefix = entry.getKey(); // HashMap Key 可以是 null
                List<Path> list = entry.getValue();
                if (null == list || list.isEmpty()) {
                    continue;
                }
                for (Path path : list) {
                    Path entity = null == prefix ? path.getFileName() : prefix.resolve(path.getFileName());
                    // 有两个参数, 第一个是文件, 第二个是在归档后的显示目录层级和文件名称, 这个参数可以随意自定义指定
                    ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(path, entity.toString());
                    zipArchiveEntry.setMethod(ZipEntry.STORED); // 不压缩, 两个不压缩不知道有什么关联?
                    parallelScatterZipCreator.addArchiveEntry(zipArchiveEntry, () -> {
                        try {
                            return Files.newInputStream(path);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    });
                }
            }

            // 写出文件
            parallelScatterZipCreator.writeTo(zipArchiveOutputStream);
            zipArchiveOutputStream.finish();
        }
    }

    /**
     * 递归列出目录下的全部文件
     */
    public static List<Path> listFilesByRecursive(Path directory) throws IOException {
        if (Files.notExists(directory)) {
            throw new RuntimeException("Path not exist");
        }
        if (!Files.isDirectory(directory)) {
            throw new RuntimeException("Path is not a directory");
        }
        List<Path> list = new LinkedList<>();
        // 遍历目录树
        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
            // 深度优先遍历文件树, 执行顺序如下
            // preVisitDirectory: 初次进入某目录 Parent 时调用, 调用后开始遍历该目录内的路径(目录和文件), 如果是目录则继续进入(深度优先), 如果是文件则调用 visitFile
            // visitFile: 遍历到的路径是文件时调用
            // visitFileFailed: 遍历到的路径是文件且无法访问文件的信息时调用
            // postVisitDirectory: 遍历完 Parent 内的所有路径(目录和文件)后调用
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                list.add(file);
                return super.visitFile(file, attrs);
            }
        });
        return list;
    }

}
